<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>JSDoc: Source: utils/soap.js</title>

    <script src="scripts/prettify/prettify.js"> </script>
    <script src="scripts/prettify/lang-css.js"> </script>
    <!--[if lt IE 9]>
      <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
    <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
    <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>

<body>

<div id="main">

    <h1 class="page-title">Source: utils/soap.js</h1>

    



    
    <section>
        <article>
            <pre class="prettyprint source linenums"><code>const Xml2js = require('xml2js')
const Crypto = require('crypto')
const Save = require('./save-xml')
const Request = require('request')
const Config = require('./config')

/**
 * SOAP management (sending, receiving, parsing) class for ONVIF modules.
 */
class Soap {
  constructor () {
    this.username = ''
    this.password = ''
    this.HTTP_TIMEOUT = 3000 // ms
  }

  /**
   * Internal method for parsing SOAP responses.
   * @param {string} soap The XML to parse.
   */
  parse (soap) {
    let promise = new Promise((resolve, reject) => {
      let prefix = soap.substring(0, 2)
      if (prefix === '--') {
        // this is a multi-part xml with attachment
        // it is up to the receiver to parse. This is
        // usually from GetSystemLog and the binary
        // part could be anything. Reference your
        // camera specs for dealing with it.
        resolve({ raw: true, soap })
      }
      else {
        let opts = {
          'explicitRoot': false,
          'explicitArray': false,
          // 'ignoreAttrs'      : true,
          'ignoreAttrs': false,
          'tagNameProcessors': [function (name) {
            // strip namespaces
            /* eslint-disable no-useless-escape */
            let m = name.match(/^([^\:]+)\:([^\:]+)$/)
            /* eslint-enable no-useless-escape */
            return (m ? m[2] : name)
          }]
        }

        // console.log(soap)
        Xml2js.parseString(soap, opts, (error, results) => {
          if (error) {
            error.soap = soap
            reject(error)
          }
          else {
            resolve({ parsed: results, soap })
          }
        })
      }
    })
    return promise
  };

  /**
   * Internal method used by the module classes.
   * @param {object} params Object containing required parameters to create a SOAP request.
   * @param {string} params.body Description in the &amp;lt;s:Body&amp;gt; of the generated xml.
   * @param {array} params.xmlns A list of xmlns attributes used in the body
   *            e.g., xmlns:tds="http://www.onvif.org/ver10/device/wsdl".
   * @param {number} params.diff Time difference [ms].
   * @param {string} params.username The user name.
   * @param {string} params.password The user Password.
   * @param {string=} params.subscriptionId To string (ex: used in Events#pullMessages).
   * @param {string=} params.subscriptionId.Address Action string (ex: used in Events#pullMessages).
   * @param {string=} params.subscriptionId._ MessageID string (ex: used in Events#pullMessages).
   * @param {string=} params.subscriptionId.$ MessageID string (ex: used in Events#pullMessages).
   */
  createRequest (params) {
    let soap = ''
    soap += '&lt;?xml version="1.0" encoding="UTF-8"?>'
    soap += '&lt;s:Envelope'
    soap += ' xmlns:s="http://www.w3.org/2003/05/soap-envelope"'
    soap += ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
    soap += ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
    soap += ' xmlns:wsa5="http://www.w3.org/2005/08/addressing"'
    soap += ' xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"'
    soap += ' xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"'
    soap += ' xmlns:tt="http://www.onvif.org/ver10/schema"'
    soap += ' xmlns:ter="http://www.onvif.org/ver10/error"'
    if (params['xmlns'] &amp;&amp; Array.isArray(params['xmlns'])) {
      params['xmlns'].forEach((ns) => {
        // make sure we don't add it twice (hikvision will fail)
        let index = soap.indexOf(ns)
        if (index &lt; 0) {
          soap += ' ' + ns
        }
      })
    }
    soap += '>'
    soap += '&lt;s:Header>'
    if (params.subscriptionId) {
      let address = this.getAddress(params.subscriptionId)
      if (address) {
        soap += '&lt;wsa5:To s:mustUnderstand="true">'
        soap += address
        soap += '&lt;/wsa5:To>'
      }
      // if (params.subscriptionId) {
      //   soap += '&lt;Action s:mustUnderstand="1">http://www.onvif.org/ver10/events/wsdl/PullPointSubscription/PullMessagesRequest&lt;/Action>'
      // }
    }

    // TODO: sample of what should be here
    // soap += '&lt;wsa5:Action s:mustUnderstand="1">'
    // soap += 'http://www.onvif.org/ver10/events/wsdl/EventPortType/CreatePullPointSubscriptionRequest'
    // soap += '&lt;/wsa5:Action>'
    // soap += '&lt;wsa5:MessageID>'
    // soap += 'urn:uuid:cca999f8-b0e1-4e4e-ac7e-04a074d49fbf'
    // soap += '&lt;/wsa5:MessageID>'
    soap += '&lt;wsa5:ReplyTo>'
    soap += '&lt;wsa5:Address>http://www.w3.org/2005/08/addressing/anonymous&lt;/wsa5:Address>'
    soap += '&lt;/wsa5:ReplyTo>'
    if (params['username']) {
      this.username = params['username']
      this.password = params['password']
      soap += this.createUserToken(params['diff'], params['username'], params['password'])
    }
    soap += '&lt;/s:Header>'
    soap += '&lt;s:Body>' + params['body'] + '&lt;/s:Body>'
    soap += '&lt;/s:Envelope>'

    /* eslint-disable no-useless-escape */
    soap = soap.replace(/\>\s+\&lt;/g, '>&lt;')
    /* eslint-enable no-useless-escape */
    return soap
  };

  /**
   * Internal method to send a SOAP request to the specified serviceAddress.
   * @param {object} service The service name.
   * @param {object} serviceAddress The service address.
   * @param {string} methodName The request name.
   * @param {xml} soapEnvelope The request SOAP envelope.
   * @param {object=} params Used internally.
   */
  makeRequest (service, serviceAddress, methodName, soapEnvelope, params) {
    let promise = new Promise((resolve, reject) => {
      Save.saveXml(service, methodName + '.Request', soapEnvelope)
      let xml = ''
      return this.runRequest(service, serviceAddress, methodName, soapEnvelope)
        .then(results => {
          xml = results
          return this.parse(xml)
        })
        // results for parse
        .then(results => {
          // is this 'raw' data?
          if ('raw' in results) {
            Save.saveXml(service, methodName + '.Response', results.soap)
            resolve(results)
            return
          }

          let fault = this.getFault(results['parsed'])
          if (fault) {
            Save.saveXml(service, methodName + '.Error', xml)
            let err = new Error(`${methodName}`)
            err.fault = fault
            err.soap = xml
            reject(err)
          }
          else {
            let parsed = this.parseResponse(methodName, results['parsed'])
            if (parsed) {
              let res = {
                'soap': xml,
                'schemas': results['parsed']['$'] ? results['parsed']['$'] : '',
                'data': parsed
              }
              Save.saveXml(service, methodName + '.Response', xml)
              resolve(res)
            }
            else {
              let err = new Error(methodName + ':The device seems to not support the ' + methodName + '() method.')
              reject(err)
            }
          }
        })
        .catch(error => {
          reject(error)
        })
    })
    return promise
  };

  /**
   * Internal method to send a SOAP request.
   * @param {object} service The service.
   * @param {object} serviceAddress The service address.
   * @param {string} methodName The request name.
   * @param {xml} soapEnvelope The request SOAP envelope.
   */
  runRequest (service, serviceAddress, methodName, soapEnvelope) {
    return new Promise((resolve, reject) => {
      if (Config.isTest()) {
        // in testing mode (for Jest)
        const Fs = require('fs')
        const Path = require('path')
        const testCameraType = Config.getCameraType()
        const testService = service
        let filePath = Path.resolve(__dirname, `../../test/data/xml/${testCameraType}/${testService}/${methodName}.Response.xml`)
        // see if the file exists
        if (!Fs.existsSync(filePath)) {
          // see if there is an Error file
          filePath = Path.resolve(__dirname, `../../test/data/xml/${testCameraType}/${testService}/${methodName}.Error.xml`)
        }

        if (!Fs.existsSync(filePath)) {
          throw new Error(`File does not exist for test: ${filePath}`)
        }

        // it's good, read it in
        let xml = Fs.readFileSync(filePath, 'utf8')
        resolve(xml)
      }
      else {
        // some cameras enable HTTP digest or digest realm,
        // so using 'Request' to handle this for us.
        let options = {
          method: 'POST',
          uri: serviceAddress.href,
          // gzip: true,
          encoding: 'utf8',
          headers: {
            'Content-Type': 'application/soap+xml; charset=utf-8;',
            'Content-Length': Buffer.byteLength(soapEnvelope)
          },
          body: soapEnvelope,
          auth: {
            user: this.username,
            pass: this.password,
            sendImmediately: false
          }
        }

        Request(options, (error, response, body) => {
          if (error) {
            console.error(error)
            reject(error)
          }
          else {
            resolve(body)
          }
        })
      }
    })
  };

  parseResponse (methodName, response) {
    let s0 = response['Body']
    if (!s0) {
      return null
    }
    let responseName = methodName + 'Response'
    if (responseName in s0) {
      return s0
    }
    else {
      return null
    }
  };

  /**
   * Parses results to see if there is a fault.
   * @param {object} results The results of a communication with a server.
   */
  getFault (results) {
    let fault = ''
    if ('Fault' in results['Body']) {
      let bodyFault = results['Body']['Fault']
      let r1 = this.parseForReason(bodyFault)
      let c1 = this.parseForCode(bodyFault)
      let d1 = this.parseForDetail(bodyFault)
      fault = {}
      fault.reason = r1
      fault.code = c1
      fault.detail = d1
    }
    return fault
  };

  parseForCode (fault) {
    let code = ''
    if ('Code' in fault) {
      let faultCode = fault['Code']
      if ('Value' in faultCode) {
        let faultValue = faultCode['Value']
        if ('Subcode' in faultCode) {
          let faultSubcode = faultCode['Subcode']
          if ('Value' in faultSubcode) {
            let faultSubcodeValue = faultSubcode['Value']
            code = faultValue + '|' + faultSubcodeValue
          }
          else {
            code = faultSubcode
          }
        }
        else {
          code = faultValue
        }
      }
    }

    return code
  }

  parseForDetail (fault) {
    let detail = ''
    if ('Detail' in fault) {
      let faultDetail = fault['Detail']
      if ('Text' in faultDetail) {
        let faultText = faultDetail['Text']
        if (typeof faultText === 'string') {
          detail = faultText
        }
        else if (typeof faultText === 'object' &amp;&amp; '_' in faultText) {
          detail = faultText['_']
        }
      }
    }

    return detail
  }

  parseForReason (fault) {
    let reason = ''
    if ('Reason' in fault) {
      let faultReason = fault['Reason']
      if ('Text' in faultReason) {
        let faultText = faultReason['Text']
        if (typeof faultText === 'string') {
          reason = faultText
        }
        else if (typeof faultText === 'object' &amp;&amp; '_' in faultText) {
          reason = faultText['_']
        }
      }
    }
    else if ('faultstring' in fault) {
      reason = fault['faultstring']
    }

    return reason
  }

  /**
   * Internal method used to create the user token xml.
   * @param {integer} diff The server timeDiff [ms].
   * @param {string} user The user name.
   * @param {string=} pass The user password.
   */
  createUserToken (diff, user, pass) {
    if (!diff) {
      diff = 0
    }
    if (!pass) {
      pass = ''
    }
    let created = (new Date(Date.now() + diff)).toISOString()
    // 10 second expiry
    let expires = (new Date(Date.now() + diff + 10000)).toISOString()
    let nonceBuffer = this.createNonce(16)
    let nonceBase64 = nonceBuffer.toString('base64')
    let shasum = Crypto.createHash('sha1')
    shasum.update(Buffer.concat([nonceBuffer, Buffer.from(created), Buffer.from(pass)]))
    let digest = shasum.digest('base64')
    let soap = ''
    soap += '&lt;wsse:Security s:mustUnderstand="1">'
    soap += '  &lt;wsu:Timestamp wsu:Id="Time">'
    soap += '    &lt;wsu:Created>' + created + '&lt;/wsu:Created>'
    soap += '    &lt;wsu:Expires>' + expires + '&lt;/wsu:Expires>'
    soap += '  &lt;/wsu:Timestamp>'
    soap += '  &lt;wsse:UsernameToken wsu:Id="User">'
    soap += '    &lt;wsse:Username>' + user + '&lt;/wsse:Username>'
    soap += '    &lt;wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + digest + '&lt;/wsse:Password>'
    soap += '    &lt;wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + nonceBase64 + '&lt;/wsse:Nonce>'
    soap += '    &lt;wsu:Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' + created + '&lt;/wsu:Created>'
    soap += '  &lt;/wsse:UsernameToken>'
    soap += '&lt;/wsse:Security>'
    return soap
  };

  createNonce (digit) {
    let nonce = Buffer.alloc(digit)
    for (let i = 0; i &lt; digit; i++) {
      nonce.writeUInt8(Math.floor(Math.random() * 256), i)
    }
    return nonce
  };

  getAddress (subscriptionid) {
    if (subscriptionid) {
      if (subscriptionid.Address) {
        return subscriptionid.Address
      }
    }
    return null
  }

  getCustomSubscriptionIdXml (subscriptionId) {
    if (subscriptionId) {
      if (subscriptionId._) {
        let id = subscriptionId._
        let xml = null
        if (subscriptionId.$) {
          let keys = Object.keys(subscriptionId.$)
          let tag = keys[0]
          let url = subscriptionId.$[tag]
          if (id &amp;&amp; tag &amp;&amp; url) {
            let tags = tag.split(':')
            // don't need Action tag
            xml = '&lt;SubscriptionId s:mustUnderstand="1" s:IsReferenceParameter="1" ' + tags[0] + '="' + url + '">' + id + '&lt;/SubscriptionId>'
            console.log(xml)
          }
        }
        return xml
      }
    }
    return null
  }
}

module.exports = Soap
</code></pre>
        </article>
    </section>




</div>

<nav>
    <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Access.html">Access</a></li><li><a href="AccessRules.html">AccessRules</a></li><li><a href="Action.html">Action</a></li><li><a href="Analytics.html">Analytics</a></li><li><a href="Camera.html">Camera</a></li><li><a href="Core.html">Core</a></li><li><a href="Credential.html">Credential</a></li><li><a href="DeviceIO.html">DeviceIO</a></li><li><a href="Discovery.html">Discovery</a></li><li><a href="Display.html">Display</a></li><li><a href="Door.html">Door</a></li><li><a href="Events.html">Events</a></li><li><a href="Imaging.html">Imaging</a></li><li><a href="Media.html">Media</a></li><li><a href="Media2.html">Media2</a></li><li><a href="OnvifManager.html">OnvifManager</a></li><li><a href="Ptz.html">Ptz</a></li><li><a href="Receiver.html">Receiver</a></li><li><a href="Recording.html">Recording</a></li><li><a href="Replay.html">Replay</a></li><li><a href="Schedule.html">Schedule</a></li><li><a href="Search.html">Search</a></li><li><a href="Security.html">Security</a></li><li><a href="Snapshot.html">Snapshot</a></li><li><a href="Soap.html">Soap</a></li><li><a href="Thermal.html">Thermal</a></li><li><a href="VideoAnalytics.html">VideoAnalytics</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>

<br class="clear">

<footer>
    Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Tue Feb 27 2018 14:01:19 GMT-0700 (MST)
</footer>

<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>
